package com.bizmda.bizsip.app.controlrule;

import cn.hutool.core.convert.Convert;
import cn.hutool.core.date.DateUtil;
import cn.hutool.extra.spring.SpringUtil;
import com.bizmda.bizsip.common.BizException;
import com.bizmda.bizsip.common.BizResultEnum;
import com.open.capacity.redis.util.RedisUtil;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;

import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

/**
 * @author shizhengye
 */
@Data
@Slf4j
public class ControlRuleMetricConfig {
    private static final String METRIC_KEY = ":metric:";
    private static final String ZSET_KEY = ":zset:";
    private static final String LIST_KEY = ":list:";
    private static final String LOCK_KEY = ":lock:";
    private String name;
    private String desc;
    private long flidingWindowTime;
    private char fixedWindowTime;
    public static final char FIXED_WINDOW_TIME_YEAR = 'y';
    public static final char FIXED_WINDOW_TIME_MONTH = 'M';
    public static final char FIXED_WINDOW_TIME_DAY = 'd';
    public static final char FIXED_WINDOW_TIME_HOUR = 'H';
    private static RedisUtil redisUtil = null;
    private static RedissonClient redissonClient = null;
    private static final String PRE_REDIS_KEY = "bizsip:ctl:metric:";
    private static final String METRIC_TYPE_COUNT = "count";
    private static final String METRIC_TYPE_SCORE = "total-score";
    private static final String METRIC_TYPE_Z_CARD = "z-card";

    public ControlRuleMetricConfig(Map<String, Object> map) {
        if (redisUtil == null) {
            redisUtil = SpringUtil.getBean(RedisUtil.class);
        }
        if (redissonClient == null) {
            redissonClient = SpringUtil.getBean(RedissonClient.class);
        }
        this.name = (String) map.get("name");
        this.desc = (String) map.get("desc");
        this.flidingWindowTime = Convert.toLong(map.get("fliding-window-time"), 0L);
        this.fixedWindowTime = Convert.toChar(map.get("fixed-window-time"), ' ');
    }

    public Object getMetric(String type, String key) throws BizException {
        ControlRuleMetric controlRuleMetric;
        String mKey = PRE_REDIS_KEY + this.name + METRIC_KEY + key;
        String zKey = PRE_REDIS_KEY + this.name + ZSET_KEY + key;
        String lKey = PRE_REDIS_KEY + this.name + LIST_KEY + key;
        switch (type) {
            case METRIC_TYPE_COUNT:
                refreshListWithTime(key);
                return redisUtil.lGetListSize(lKey);
            case METRIC_TYPE_SCORE:
                refreshListWithTime(key);
                controlRuleMetric = (ControlRuleMetric) redisUtil.get(mKey);
                if (controlRuleMetric == null) {
                    return 0.0d;
                }
                return controlRuleMetric.getScore();
            case METRIC_TYPE_Z_CARD:
                refreshListWithTime(key);
                return redisUtil.zCard(zKey);
            default:
                throw new BizException(BizResultEnum.CONTROL_RULE_METRIC_TYPE_ERROR);
        }
    }

    private void refreshListWithTime(String key) throws BizException {
        String mKey = PRE_REDIS_KEY + this.name + METRIC_KEY + key;
        String lKey = PRE_REDIS_KEY + this.name + LIST_KEY + key;
        String zKey = PRE_REDIS_KEY + this.name + ZSET_KEY + key;
        String lockKey = PRE_REDIS_KEY + this.name + LOCK_KEY + key;
        RLock rLock = redissonClient.getLock(lockKey);
        long now = System.currentTimeMillis();
        long count = 0;
        double totalScore = 0;
        String key2;
        this.lock(rLock);
        for (; ; ) {
            long size = redisUtil.lGetListSize(lKey);
            if (size == 0) {
                redisUtil.del(zKey);
                redisUtil.del(mKey);
                break;
            }
            ControlRuleRecord controlRuleRecord1 = (ControlRuleRecord)
                    redisUtil.lGetIndex(lKey, 0L);
            if (controlRuleRecord1 == null) {
                redisUtil.del(lKey);
                continue;
            }
            if (this.inWindowTime(now, controlRuleRecord1.getTime())) {
                break;
            }
            count++;
            controlRuleRecord1 = (ControlRuleRecord) redisUtil.lLeftPop(lKey);

            totalScore = totalScore + controlRuleRecord1.getScore();
            key2 = controlRuleRecord1.getKey2();
            if (key2 != null) {
                redisUtil.incrementScore(zKey,key2,-controlRuleRecord1.getScore());
            }
        }
        if (count > 0) {
            ControlRuleMetric controlRuleMetric = (ControlRuleMetric) redisUtil.get(mKey);
            if (controlRuleMetric == null) {
                controlRuleMetric = this.initMetric();
            } else {
                controlRuleMetric.setScore(controlRuleMetric.getScore() - totalScore);
            }
            redisUtil.set(mKey, controlRuleMetric);
        }
        this.unlock(rLock);
    }

    public void addRecord(String key, double score) throws BizException {
        String mKey = PRE_REDIS_KEY + this.name + METRIC_KEY + key;

        ControlRuleRecord controlRuleRecord = new ControlRuleRecord();
        controlRuleRecord.setTime(System.currentTimeMillis());
        controlRuleRecord.setScore(score);
        String lKey = PRE_REDIS_KEY + this.name + LIST_KEY + key;
        String lockKey = PRE_REDIS_KEY + this.name + LOCK_KEY + key;
        RLock rLock = redissonClient.getLock(lockKey);
        this.lock(rLock);
        redisUtil.lSet(lKey, controlRuleRecord);
        ControlRuleMetric controlRuleMetric = (ControlRuleMetric) redisUtil.get(mKey);
        if (controlRuleMetric == null) {
            controlRuleMetric = this.initMetric();
        }
        controlRuleMetric.setScore(controlRuleMetric.getScore() + controlRuleRecord.getScore());
        redisUtil.set(mKey, controlRuleMetric);
        this.unlock(rLock);
        this.debugRedis(key);
    }

    public void addRecord(String key1, String key2, double score) throws BizException {
        String mKey = PRE_REDIS_KEY + this.name + METRIC_KEY + key1;

        ControlRuleRecord controlRuleRecord = new ControlRuleRecord();
        controlRuleRecord.setTime(System.currentTimeMillis());
        controlRuleRecord.setKey2(key2);
        controlRuleRecord.setScore(score);
        String lKey = PRE_REDIS_KEY + this.name + LIST_KEY + key1;
        String zKey = PRE_REDIS_KEY + this.name + ZSET_KEY + key1;

        String lockKey = PRE_REDIS_KEY + this.name + LOCK_KEY + key1;
        RLock rLock = redissonClient.getLock(lockKey);

        this.lock(rLock);

        redisUtil.incrementScore(zKey,key2,score);
        redisUtil.lSet(lKey, controlRuleRecord);
        ControlRuleMetric controlRuleMetric = (ControlRuleMetric) redisUtil.get(mKey);
        if (controlRuleMetric == null) {
            controlRuleMetric = this.initMetric();
        }
        controlRuleMetric.setScore(controlRuleMetric.getScore() + controlRuleRecord.getScore());
        redisUtil.set(mKey, controlRuleMetric);
        this.unlock(rLock);
        this.debugRedis(key1);
    }
    
    private ControlRuleMetric initMetric() {
        return new ControlRuleMetric();
    }

    private boolean inWindowTime(long currentTime, long recordTime) {
        if (this.flidingWindowTime > 0) {
            return (currentTime - recordTime) < this.flidingWindowTime;
        }
        switch (this.fixedWindowTime) {
            case FIXED_WINDOW_TIME_YEAR:
                return DateUtil.format(DateUtil.date(currentTime), "yyyy")
                        .equals(DateUtil.format(DateUtil.date(recordTime), "yyyy"));
            case FIXED_WINDOW_TIME_MONTH:
                return DateUtil.format(DateUtil.date(currentTime), "yyyyMM")
                        .equals(DateUtil.format(DateUtil.date(recordTime), "yyyyMM"));
            case FIXED_WINDOW_TIME_DAY:
                return DateUtil.format(DateUtil.date(currentTime), "yyyyMMdd")
                        .equals(DateUtil.format(DateUtil.date(recordTime), "yyyyMMdd"));
            case FIXED_WINDOW_TIME_HOUR:
                return DateUtil.format(DateUtil.date(currentTime), "yyyyMMddHH")
                        .equals(DateUtil.format(DateUtil.date(recordTime), "yyyyMMddHH"));
            default:
                return false;
        }
    }

    private void debugRedis(String key) {
        if (!log.isTraceEnabled()) {
            return;
        }
        String mKey = PRE_REDIS_KEY + this.name + METRIC_KEY + key;
        ControlRuleMetric controlRuleMetric = (ControlRuleMetric) redisUtil.get(mKey);
        log.trace("{}: {}", mKey, controlRuleMetric);

        String lKey = PRE_REDIS_KEY + this.name + LIST_KEY + key;
        long size = redisUtil.lGetListSize(lKey);
        log.trace("{}:", lKey);
        for (long i = 0; i < size; i++) {
            ControlRuleRecord controlRuleRecord1 = (ControlRuleRecord)
                    redisUtil.lGetIndex(lKey, i);
            log.trace("{}:{}", i, controlRuleRecord1);
        }
        String zKey = PRE_REDIS_KEY + this.name + ZSET_KEY + key;
        log.trace("{}:", zKey);
        Set<Object> set = redisUtil.zRange(zKey, 0, -1);
        if (set != null) {
            for (Object value : set) {
                log.trace("{}:{}", value, redisUtil.zGetScore(zKey, value));
            }
        }
    }

    private void lock(RLock rLock) throws BizException {
        boolean res;
        try {
            res = rLock.tryLock(5, 3, TimeUnit.SECONDS);
            if (!res) {
                throw new BizException(BizResultEnum.OTHER_LOCK_ERROR);
            }
        } catch (InterruptedException e) {
            log.warn("Interrupted!",e);
            Thread.currentThread().interrupt();
        }
    }

    private void unlock(RLock rLock) {
        rLock.unlock();
    }
}
